package com.crane.framework.aspect;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.crane.framework.entity.Log;
import com.crane.framework.response.CommonEnum;
import com.crane.framework.response.CommonResult;
import com.crane.framework.service.ILogService;
import com.crane.framework.utils.ByteUtils;
import com.crane.framework.utils.CommonUtil;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.*;

import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import static java.lang.Thread.sleep;

/**
 * 系统日志切面
 *
 * @author CL
 */
@Log4j2
@Aspect
@Component("LogAspect")
@SuppressWarnings({"all"})
public class LogAspect {

  private final ThreadLocal<Log> sysLogThreadLocal = new InheritableThreadLocal<>();

  @Resource private ILogService logService;

  private static final String threadNamePrefix = "crane-log-pool";

  ThreadFactory threadFactory =
      new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").setDaemon(true).build();

  ExecutorService executorService =
      new ThreadPoolExecutor(
          2,
          10,
          1,
          TimeUnit.MINUTES,
          new ArrayBlockingQueue<>(5, true),
          threadFactory, // 使用自定义的 ThreadFactory
          new ThreadPoolExecutor.CallerRunsPolicy());

  @Pointcut("execution(public * com.crane.*.controller.*.*(..))")
  public void logAspect() {}

  @Before(value = "logAspect()")
  public void doBefore(JoinPoint joinPoint) throws Throwable {
    HttpServletRequest request =
        ((ServletRequestAttributes)
                Objects.requireNonNull(RequestContextHolder.getRequestAttributes()))
            .getRequest();

    Log sysLog = new Log();
    sysLog.setCreateUserCode("");
    sysLog.setCreateUserName("");
    sysLog.setStartTime(LocalDateTime.now());
    sysLog.setCreateDate(LocalDateTime.now());
    sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
    sysLog.setRequestParams(formatParams(request.getParameterMap()));
    sysLog.setRequestMethod(request.getMethod());
    sysLog.setRequestIp(request.getRemoteAddr());
    sysLog.setServerAddress(
        request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort());
    String userAgentStr = request.getHeader("User-Agent");
    sysLog.setUserAgent(userAgentStr);
    UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
    sysLog.setDeviceName(userAgent.getOs().getName());
    sysLog.setBrowserName(userAgent.getBrowser().getName());
    try {
      sysLog.setCreateUserCode(StpUtil.getLoginId().toString());
    } catch (Exception e) {
      log.error("该接口没有token");
    }

    sysLogThreadLocal.set(sysLog);

    log.info(
        "开始计时: {}  URI: {}  IP: {}",
        sysLog.getStartTime(),
        sysLog.getRequestUri(),
        sysLog.getRequestIp());
  }

  @AfterReturning(pointcut = "logAspect()", returning = "ret")
  public void doAfterReturning(Object ret) {
    Log sysLog = sysLogThreadLocal.get();
    sysLog.setLogType("INFO");
    sysLog.setEndTime(LocalDateTime.now());
    sysLog.setExecuteTime(ChronoUnit.MILLIS.between(sysLog.getStartTime(), sysLog.getEndTime()));
    if (CommonUtil.notNullOrEmpty(ret)) {
      CommonResult<?> r = null;
      if (ret instanceof CommonResult) {
        r = (CommonResult) ret;
      } else {
        r = new CommonResult(ret);
      }
      if (Objects.equals(CommonEnum.FAIL_EXCEPTION.getCode(), r.getCode())) {
        sysLog.setIsException(true);
        sysLog.setExceptionInfo(r.getMsg());
      } else {
        sysLog.setIsException(false);
      }
    }

    executorService.execute(
        () -> {
          try {
            logService.save(sysLog);
            sysLogThreadLocal.remove();
          } catch (Exception e) {
            log.error("Failed to save log.", e);
          }
        });

    Runtime runtime = Runtime.getRuntime();
    log.info(
        "计时结束: {}  用时: {}ms  URI: {}  总内存: {}  已用内存: {}",
        sysLog.getEndTime(),
        sysLog.getExecuteTime(),
        sysLog.getRequestUri(),
        ByteUtils.formatByteSize(runtime.totalMemory()),
        ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
  }

  @AfterThrowing(pointcut = "logAspect()", throwing = "e")
  public void doAfterThrowable(Throwable e) {
    Log sysLog = sysLogThreadLocal.get();
    sysLog.setLogType("ERROR");
    sysLog.setEndTime(LocalDateTime.now());
    sysLog.setExecuteTime(ChronoUnit.MINUTES.between(sysLog.getStartTime(), sysLog.getEndTime()));
    sysLog.setIsException(true);
    sysLog.setExceptionInfo(e.getMessage());
    executorService.execute(
        () -> {
          try {
            logService.save(sysLog);
            sysLogThreadLocal.remove();
          } catch (Exception err) {
            log.error("Failed to save log.", err);
          }
        });

    Runtime runtime = Runtime.getRuntime();
    log.info(
        "计时结束: {}  用时: {}ms  URI: {}  总内存: {}  已用内存: {}",
        sysLog.getEndTime(),
        sysLog.getExecuteTime(),
        sysLog.getRequestUri(),
        ByteUtils.formatByteSize(runtime.totalMemory()),
        ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
  }

  private String formatParams(Map<String, String[]> parameterMap) {
    if (parameterMap == null) {
      return null;
    }
    StringBuilder params = new StringBuilder();
    for (Map.Entry<String, String[]> param : (parameterMap).entrySet()) {
      if (params.length() != 0) {
        params.append("&");
      }
      params.append(param.getKey()).append("=");
      if (StrUtil.endWithIgnoreCase(param.getKey(), "password")) {
        params.append("*");
      } else if (param.getValue() != null) {
        params.append(ArrayUtil.join(param.getValue(), ","));
      }
    }
    return params.toString();
  }
}
